# encoding: utf-8

import codecs


class Work:
    def __init__(self):
        self.composer = ''
        self.form = ''
        self.key = ''
        self.opusus = ''
        self.number = ''
        self.title = ''
        self.movements = []

    def display(self):
        return '\t'.join(filter(lambda x: x,
            [self.key, self.opus, self.number, self.title, self.form, self.composer]))

    def dump(self):
        print self.display()
        for i, m in enumerate(self.movements):
            print '  %d. %s' % (i + 1, m)

def attrs_from_line(line, def_key='title'):
    # attr1:value1;attr2:value2;... -> { 'attr1': 'value1', 'attr2': 'value2', ... }
    d = {}
    for kv in [kv for kv in line.split(';') if len(kv) > 0]:
        if ':' not in kv:
            d[def_key] = kv
        else:
            k, v = kv.split(':')
            d[k] = v
    return d

class WorkParserError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class Tag(dict):
    def __init__(self, name, attrs):
        self.name = name
        if attrs is not None:
            self.update(attrs)
    def is_suite(self):
        return self.name == 'suite'
    def is_colletion(self):
        return self.name == 'colletion'
    def is_composer(self):
        return self.name == 'composer'

class WorkParser():

    def __init__(self):
        self.works = []
        self.tag_stack = []
        self.__composer = ''
        self.__is_suite = False
        self.__movements = []

    def __add_tags_attrs(self, attrs):
        """ Add the attributes from tags in tag stack.

        Args:
          attrs Attributes dict of a work item.
        """
        for tag in self.tag_stack:
            if not tag.is_suite():
                attrs.update(tag)
        return attrs

    def __parse_tag(self, line):
        """
        Args:
          line A line started with { and ended with }. E.g.,
            - {form:concerto-piano}
            - {form} # end tag
            - {suite|form:symphony;key:D minor;op:125;nr.9;title:Choral%合唱}
        Returns:
            Tag object.
        """
        if line[-1] != '}':
            raise WorkParserError('Tag should end with }')
        line = line[1:-1] # strip { & }
        if len(line) == 0:
            raise WorkParserError('Empty tag')
        i = line.find('|')
        if i != -1:
            self.__is_suite = True
            return Tag(line[0:i], attrs_from_line(line[i+1:], def_key='title'))

        self.__is_suite = False
        # If not |, there can be only one k:v pair. (TODO: error handling for
        # multiple k:v pairs which are not allowed.)
        if ':' not in line: # end tag
            return Tag(line, None)
        k, v = line.split(':')
        return Tag(k, {k: v})

    def __parse_tag_line(self, line):
        tag = self.__parse_tag(line)
        if tag.is_composer():
            self.__composer = tag['composer']
            return
        if len(tag) > 0: # Start tag has attributes.
            self.tag_stack.append(tag)
        else: # End tag has no attributes.
            tag = self.tag_stack.pop()
            if tag.is_suite():
                self.works.append(self.__new_work(tag))

    def __add_movement(self, attrs):
        self.__movements.append(attrs.get('title'))

    def __new_work(self, tag):
        if not self.__composer:
            self.__movements = []
            raise WorkParserError('Composer is needed to define music works.')
        w = Work()
        w.composer = self.__composer
        w.form = tag.get('form')
        w.title = tag.get('title')
        w.key = tag.get('key')
        w.opus = tag.get('op')
        w.number = tag.get('nr')
        w.movements = self.__movements
        self.__movements = []
        return w

    def __add_work(self, attrs):
        self.works.append(self.__new_work(attrs))

    def __parse_line(self, line):
        attrs = attrs_from_line(line, def_key='title')
        attrs = self.__add_tags_attrs(attrs)
        if self.__is_suite:
            self.__add_movement(attrs)
        else:
            self.__add_work(attrs)

    def parse(self, line):
        try:
            if line[0] == '{':
                self.__parse_tag_line(line)
            else:
                self.__parse_line(line)
        except WorkParserError, e:
            print 'Error:', e

def parse_file_obj(fo):
    wp = WorkParser()
    for line in fo:
        line = line.strip(' \r\n')
        if len(line) > 0 and line[0] != '#':
            wp.parse(line)
    for work in wp.works:
        print '-' * 40
        work.dump()

def parse_work_file(fn):
    f = codecs.open('work/' + fn, 'r', 'utf-8')
    parse_file_obj(f)
    f.close()

def main():
    fns = [
        'wolfgang_amadeus_mozart.txt',
        'ludwig_van_beethoven.txt',
        'modest_mussorgsky.txt',
    ]
    for fn in fns:
        print '\n>>', fn, '\n'
        parse_work_file(fn)

if __name__ == '__main__':
    main()
